/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.classloader;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.util.ChildrenCollectorFilter;
import org.apache.jackrabbit.util.Text;

/**
 * The <code>PatternPath</code> implements a list of repository item path
 * patterns providing an iterator on the expanded paths. The list of patterns is
 * immutably configured from an array of strings.
 * <p>
 * While the original list of path patterns may be retrieved for informational
 * purposes by calling the {@link #getPath()} method, the primary contents of
 * instances of this class are the expanded paths accessible by calling the
 * {@link #getExpandedPaths()} method.
 * <p>
 * Please note that though this list is immutable there is intentionally no
 * guarantee that all invocations of the {@link #getExpandedPaths} method
 * return the same contents as the patterns contained in the list may expand to
 * different paths for each invocation of that method.
 * <p>
 * Each entry in the pattern list is a path whose segments conform to the
 * pattern syntax defined for the <code>Node.getNodes(String)</code> method.
 * The pattern may be a full name or a partial name with one or more wildcard
 * characters ("*"), or a disjunction (using the "|" character to represent
 * logical <i>OR</i>) of these. For example,
 * <blockquote><code>"jcr:*|foo:bar"</code></blockquote>
 * would match <code>"foo:bar"</code>, but also <code>"jcr:whatever"</code>.
 * <p>
 * The EBNF for pattern is:
 * <pre>
 * namePattern ::= disjunct {'|' disjunct}
 * disjunct ::= name [':' name]
 * name ::= '*' |
 *          ['*'] fragment {'*' fragment}['*']
 * fragment ::= char {char}
 * char ::= nonspace | ' '
 * nonspace ::= (* Any Unicode character except:
 *               '/', ':', '[', ']', '*',
 *               ''', '"', '|' or any whitespace
 *               character *)
 * </pre>
 *
 * @author Felix Meschberger
 * @version $Rev:$, $Date:$
 */
/* package */ class PatternPath {

    /** default logger */
    private static final Log log = LogFactory.getLog(PatternPath.class);

    /** The session to access the repository */
    private final Session session;

    /** The list of path patterns */
    private final String[] patterns;

    /**
     * Creates a <code>PatternPath</code> containing the elements of the
     * string array. Each entry in the array which is either empty or
     * <code>null</code> is ignored and not added to the list. If the array
     * is empty or only contains empty or <code>null</code> elements, the
     * resulting list will consequently be empty.
     *
     * @param session The session to access the Repository to expand the paths
     *      and to register as an event listener.
     * @param pathPatterns The array of path patterns to add.
     *
     * @throws NullPointerException if the <code>pathPatterns</code> array or
     *      the <code>session</code> is <code>null</code>.
     */
    /* package */ PatternPath(Session session, String[] pathPatterns) {

        // check session
        if (session == null) {
            throw new NullPointerException("session");
        }

        // prepare the pattern list, excluding null/empty entries
        List patternList = new ArrayList();
        for (int i=0; i < pathPatterns.length; i++) {
            addChecked(patternList, pathPatterns[i]);
        }
        patterns =
            (String[]) patternList.toArray(new String[patternList.size()]);

        this.session = session;
    }

    /**
     * Returns the session from which this instance has been constructed.
     */
    /* package */ Session getSession() {
        return session;
    }

    /**
     * Returns a copy of the list of path patterns from which this instance has
     * been constructed.
     */
    /* package */  String[] getPath() {
        return (String[]) patterns.clone();
    }

    /**
     * Returns the list of expanded paths matching the list of patterns. This
     * list is guaranteed to only return existing items.
     * <p>
     * Each invocation of this method expands the pattern anew and returns a
     * new list instance.
     *
     * @return The list of paths matching the patterns. If the pattern list is
     *      empty or if no real paths match for any entry in the list, the
     *      returned list is empty.
     *
     * @throws RepositoryException if an error occurrs expanding the path
     *      pattern list.
     */
    /* package */ List getExpandedPaths() throws RepositoryException {
        List result = new ArrayList(patterns.length);
        Node root = session.getRootNode();

        for (int i=0; i < patterns.length; i++) {
            String entry = patterns[i];

            if (entry.indexOf('*') >= 0 || entry.indexOf('|') >= 0) {

                scan(root, entry, result);

            } else {
                // add path without pattern characters without further
                // checking. This allows adding paths which do not exist yet.
                result.add(entry);
            }

        }

        return result;
    }

    //---------- Object overwrite ----------------------------------------------

    /**
     * Returns <code>true</code> if this object equals the other object. This
     * implementation only returns true if the other object is the same as this
     * object.
     * <p>
     * The difference to the base class implementation is, that we only accept
     * equality if the other object is the same than this object. This is
     * actually the same implementation as the original <code>Object.equals</code>
     * implementation.
     *
     * @param o The other object to compare to.
     *
     * @return <code>true</code> if the other object is the same as this.
     */
    public boolean equals(Object o) {
        return o == this;
    }

    /**
     * Returns a hashcode for this instance. This is currently the hash code
     * returned by the parent implementation. While it does not violate the
     * contract to not change the <code>hashCode()</code> implementation but
     * to change the implementation of the {@link #equals} method, I think this
     * is ok, because our implementation of the {@link #equals} method is just
     * more specific than the base class implementation, which also allows
     * the other object to be a list with the same contents.
     *
     * @return The hash code returned by the base class implementation.
     */
    public int hashCode() {
        return super.hashCode();
    }

    /**
     * Returns a string representation of this instance. This is actually the
     * result of the string representation of the List, this actually is,
     * prefixed with the name of this class.
     *
     * @return The string representation of this instance.
     */
    public String toString() {
        StringBuffer buf = new StringBuffer("PatternPath: [");
        for (int i=0; i < patterns.length; i++) {
            if (i != 0) buf.append(", ");
            buf.append(patterns[i]);
        }
        buf.append("]");
        return buf.toString();
    }

    //---------- internal ------------------------------------------------------

    /**
     * Adds the string to the list of patterns, if neither empty nor
     * <code>null</code>. If the string has one or more trailing slashes
     * (<em>/</em>) they are removed before adding the string.
     */
    private void addChecked(List patternList, String pattern) {
        if (pattern == null || pattern.length() == 0) {
            log.debug("addChecked: Not adding null/empty pattern");
        } else {

            // remove all trailing slashes
            while (pattern.endsWith("/") && pattern.length() > 1) {
                pattern = pattern.substring(0, pattern.length()-1);
            }

            log.debug("addChecked: Adding " + pattern);
            patternList.add(pattern);
        }
    }

    //---------- Path expansion -----------------------------------------------

    /**
     * Finds the paths of all nodes and properties matching the pattern below
     * the <code>root</code> node.
     *
     * @param root The root node of the subtree to match against the path
     *      pattern.
     * @param pathPattern The path pattern to use to find matching nodes.
     * @param gather The list into which the paths of matching child items
     *      are added.
     */
    private static void scan(Node root, String pathPattern, List gather)
            throws RepositoryException {

        // initial list of candidates is the root node
        List candidates = new ArrayList();
        candidates.add(root);

        StringTokenizer patterns = new StringTokenizer(pathPattern, "/");
        boolean moreTokens = patterns.hasMoreTokens();
        while (moreTokens) {
            String pattern = patterns.nextToken();
            moreTokens = patterns.hasMoreTokens();

            // new candidates are the children of the current candidates list
            // matching the current pattern
            List newCandidates = new ArrayList();
            for (Iterator ci=candidates.iterator(); ci.hasNext(); ) {
                Node current = (Node) ci.next();
                for (NodeIterator ni=current.getNodes(pattern); ni.hasNext(); ) {
                    newCandidates.add(ni.nextNode());
                }

                // if pattern is the last, also consider properties
                if (!moreTokens) {
                    PropertyIterator pi = current.getProperties(pattern);
                    while (pi.hasNext()) {
                        newCandidates.add(pi.nextProperty());
                    }
                }
            }

            // drop old candidates and use new for next step
            candidates.clear();
            candidates = newCandidates;
        }

        // add paths of the candidates to the gather list
        for (Iterator ci=candidates.iterator(); ci.hasNext(); ) {
            Item current = (Item) ci.next();
            gather.add(current.getPath());
        }
    }

    //---------- matching support ---------------------------------------------

    /**
     * Applies the list of path patterns to the given path returning
     * <code>true</code> if it matches, <code>false</code> otherwise.
     * <p>
     * <b><em>This method is package protected for testing purposes. This
     * method is not intended to be used by clients. Its specification or
     * implementation may change without notice.</em></b>
     *
     * @param path The path to match with the pattern list.
     *
     * @return <code>true</code> if the path matches any of the patterns.
     */
    /* package */ boolean matchPath(String path) {
        StringTokenizer exploded = new StringTokenizer(path, "/");

        OUTER_LOOP:
        for (int i=0; i < patterns.length; i++) {
            StringTokenizer exEntry = new StringTokenizer(patterns[i], "/");

            // ignore if the number of path elements to not match
            if (exploded.countTokens() != exEntry.countTokens()) {
                continue;
            }

            while (exploded.hasMoreTokens()) {
                if (!ChildrenCollectorFilter.matches(exploded.nextToken(),
                        exEntry.nextToken())) {
                    continue OUTER_LOOP;
                }
            }

            // if I get here, the path matches entry[i]
            return true;
        }

        // if we run out, no match has been found
        return false;
    }
}
